今天來説說另一個跟 ViewModel 息息相關的東西 --- LiveData。
LiveData 與 ViewModel 一樣,是一個可以感知 Activity / Fragment 生命週期的 Data Holder,因此他可以確保只在元件處在 "Active" 才會更新 UI , View 在背景時則會保存此狀態,並在下一次元件甦醒時更新畫面,而 View 被摧毀時則會一併被回收,從而避免了 memory leak。
想想以前在把資料顯示在 Activity 上時,動不動就要檢查 Activity 是否存活,如今有了 LiveData 後就可以省略這些步驟了。
dependencies {
def lifecycle_version = "2.1.0"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
kapt "androidx.lifecycle:lifecycle-compiler:$lifecycle_version"
}
如果昨天已經加入的話,這裡可以省略。
首先在 ViewModel 中加初始化一個 LiveData,並寫一個傳送資料的方法:
class TaskViewModel(private val repository: TasksRepository) : ViewModel() {
private val _dataLoading = MediatorLiveData<Boolean>()
val dataLoading: LiveData<Boolean>
get() = _dataLoading
fun loadData() {
_dataLoading.value = true
// 等一段時間後改變資料
Handler().postDelayed({
_dataLoading.value = false
}, 2500)
}
......
}
dataLoading
之所以這麼寫是為了確保在 UI 層操作 LiveData 時只能夠負責顯示資料,而不能保存資料狀態。
接著在 Activity 觀察(observe) LiveData:
class MainActivity : AppCompatActivity() {
private val repository by lazy { TasksRepository() }
private val factory by lazy { TodoViewModelFactory(repository) }
private lateinit var viewModel: TaskViewModel
......
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this, factory).get(TaskViewModel::class.java)
viewModel.dataLoading.observe(this, Observer {
if (it) {
Toast.makeText(this, "Loading...", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Done!", Toast.LENGTH_SHORT).show()
}
})
btnLiveData.setOnClickListener {
viewModel.loadData()
}
......
}
......
}
還有另一種觀察 LiveData 的方式是配合 DataBinding ,也是比較常見的做法,但是這邊先省略,之後幾天再詳細介紹。
通常我們會在 onCreate()
開始觀察 LiveData ,有以下的原因:
onResume()
而有多餘的調用。一般而言 LiveData 只會在資料改變時傳遞給觀察者,但是有例外狀況如昨天在 ViewModel 提到類似的例子:如果旋轉螢幕則因為 View 重新初始化,所以又會收到 LiveData 的資料,造成 Toast UI 重複顯示等。
Google 為此提出了一種解法,使用一個封裝類封裝 LiveData 的資料,封裝類內部在發現資料重複發送時即阻止這一次的畫面更新,具體做法如下:
open class Event<out T>(private val content: T) {
// 一個用來標示這個資料是否已更新 UI 的 flag
var hasBeenHandled = false
private set
fun getContentIfNotHandled(): T? {
return if (hasBeenHandled) {
null
} else {
hasBeenHandled = true
content
}
}
fun getContent(): T = content
}
/**
* 拓展使用 [Event] 時的 [Observer] 操作行為
*/
class EventObserver<T>(private val onEventUnhandledContent: (T) -> Unit)
: Observer<Event<T>> {
override fun onChanged(event: Event<T>?) {
event?.getContentIfNotHandled()?.let {
onEventUnhandledContent(it)
}
}
}
接著修改 Activity 的訂閱方式以及 ViewModel 傳遞的資料類型:
class TaskViewModel(private val repository: TasksRepository) : ViewModel() {
private val _dataLoading = MediatorLiveData<Event<Boolean>>()
val dataLoading: LiveData<Event<Boolean>>
get() = _dataLoading
fun loadData() {
_dataLoading.value = Event(true)
Handler().postDelayed({
_dataLoading.value = Event(false)
}, 2500)
}
......
}
class MainActivity : AppCompatActivity() {
private val repository by lazy { TasksRepository() }
private val factory by lazy { TodoViewModelFactory(repository) }
private lateinit var viewModel: TaskViewModel
......
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProviders.of(this, factory).get(TaskViewModel::class.java)
viewModel.dataLoading.observe(this, EventObserver {
if (it) {
Toast.makeText(this, "Loading...", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Done!", Toast.LENGTH_SHORT).show()
}
})
btnLiveData.setOnClickListener {
viewModel.loadData()
}
......
}
......
}
現在 Room 也可以直接返回 LiveData 類,好處是 observe 後,當 DB 資料更新時也會一併更新 LiveData ,可以讓我們很簡單就讓 UI 顯示的資料與 DB 裡的資料保持一致。
有時候在 LiveData 被訂閱前可能需要把 LiveData 裡的資料轉成另一個型態,或是需要基於某個 LiveData 實體的值回傳不同的 LiveData ,這時候就會需要 Transformations 來實現。
類似 Kotlin / RxJava 的 map
,即把 LiveData 裡的資料轉成另一個型態並送出:
val taskLiveData: LiveData<Task> = ......
val taskTitleLiveData: LiveData<String> = Transformations.map(taskLiveData) {
"Title: " + it.title
}
如果有一個情境,
搜尋某筆資料, return 一個 LiveData
實作方法如下:
fun getTask(id: String): LiveData<Task> {
return dao.getTask(id)
}
上面的方法在 View 訂閱後可以發出我們需要的資料,但是當我們搜尋不同 id 的 Task ,會回傳不同的 LiveData 回來,讓我們必須重新在訂閱一次,這時候就可以使用 switchMap()
處理,讓我們修改一下程式碼:
val idLiveData: MutableLiveData<String> = ......
val taskTitleLiveData: LiveData<Task> = MutableLiveData()
init {
taskTitleLiveData = Transformations.switchMap(idLiveData) {
getTask(it)
}
}
fun searchTask(id: String) {
idLiveData.value = id
}
fun getTask(id: String): LiveData<Task> {
return ......
}
這樣就可以根據別的 LiveData 發射的 trigger 接收資料的同時,使用同一個 LiveData instance 訂閱 LiveData。
如果有個需求:
local 或是網路的資料有更新時,需要通知 UI
這時候就可以在 local 及 network 各自建立一個 LiveData ,並將兩個資源合併到 MediatorLiveData 中。
如此一來,當 local 及 network 有變化時,只需要訂閱 MediatorLiveData
即可知道兩個來源的資料更新。
LiveData 的介紹先到這邊,其實還有許多功能沒有提到,這邊只是介紹了一些較常見的部分,明天再來説説另一個有趣的 Component。